/*
This file is part of CanFestival, a library implementing CanOpen Stack.

Copyright (C): James Steward

See COPYING file for copyrights details.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/**
    Pseudo CAN hub application.
*/

#define _GNU_SOURCE  //for asprintf()

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <termios.h>
#include <stdlib.h>
#include <dlfcn.h>

#define NEED_PRINT_MESSAGE

#ifdef DLL_CALL
    #undef DLL_CALL
#endif

#define DLL_CALL(funcname) (* funcname##_driver)

#include "canfestival.h"

int DLL_CALL(canfd)(CAN_HANDLE)FCT_PTR_INIT;

#ifdef DLL_CALL
    #undef DLL_CALL
#endif

#define DLL_CALL(funcname) funcname##_driver

#define DLSYM(name)\
    *(void **) (&name##_driver) = dlsym(handle, #name"_driver");\
    if ((error = dlerror()) != NULL)  {\
        fprintf (stderr, "%s\n", error);\
        UnLoadCanDriver(handle);\
        return NULL;\
    }

#define MAX_HUB_PORTS 16

typedef struct
{
    int fd;
    struct termios old_termio, new_termio;
    char* name;
} port;

port hub_ports[MAX_HUB_PORTS + 1];  //An extra for a CAN driver port

s_BOARD bus_if = { "/dev/ttyS0", "125K" };

/* Receive a CAN message from a hub (pseudo) port */
int hub_receive(port* p, Message* m)
{
    int rv, N, n = 0;
    fd_set rfds;
    struct timeval tv;
    N = 4; //initially try to read 4 bytes, including the length byte
retry:
    rv = read(p->fd, &((char*)m)[n], N - n);
    if (rv == -1)
    {
        fprintf(stderr, "read: %d, %s\n", p->fd, strerror(errno));
        return -1;
    }
    n += rv;
    if (n == 4)
    {
        N = (4 + m->len);
        if (m->len > 8)
        {
            fprintf(stderr, "Warning: invalid message length %d\n",
                    m->len);
            //try to resync
            return 0;
        }
    }
    if (n < N)
    {
        FD_ZERO(&rfds);
        FD_SET(p->fd, &rfds);
        tv.tv_sec = 0;
        tv.tv_usec = 100000;
        rv = select(p->fd + 1, &rfds, NULL, NULL, &tv);
        if (rv == 0 || rv == -1)
        {
            fprintf(stderr, "select: %s\n", strerror(errno));
            return 0;
        }
        goto retry;
    }
    return 1;
}

/* send a CAN message to one of the hub ports */
UNS8 hub_send(port* p, Message* m)
{
    int rv;
    rv = write(p->fd, m, 4 + m->len);
    if (rv != 4 + m->len)
    {
        return 1;
    }
    return 0;
}

/* Open a hub port */
int hub_open(port* p)
{
    if (p->fd != -1)
    {
        fprintf(stderr, "Warning, port %s is already open, fd %d!\n",
                p->name, p->fd);
    }
    p->fd = open(p->name, O_RDWR);
    if (p->fd < 0)
    {
        fprintf(stderr, "open: %s, %s\n", p->name, strerror(errno));
        goto exit_here;
    }
    if (tcgetattr(p->fd, &p->old_termio) != 0)
    {
        fprintf(stderr, "tcgetattr: %s, %s\n",
                p->name, strerror(errno));
        close(p->fd);
        p->fd = -1;
        goto exit_here;
    }
    memcpy(&p->new_termio, &p->old_termio, sizeof(p->old_termio));
    cfmakeraw(&p->new_termio);
    cfsetispeed(&p->new_termio, B115200);
    cfsetospeed(&p->new_termio, B115200);
    tcsetattr(p->fd, TCSANOW, &p->new_termio);
exit_here:
    return p->fd;
}

/* Close a hub port*/
int hub_close(port* p)
{
    if (p->fd >= 0)
    {
        tcsetattr(p->fd, TCSANOW, &p->old_termio);
        close(p->fd);
        p->fd = -1;
    }
    return 0;
}

/** Read from the port index rd_port, and write to all other ports. */
int read_write(int rd_port, port* p, CAN_HANDLE h, fd_set* wfds, int max_fd)
{
    Message m;
    int rv, i;
    fd_set wfds_copy;
    struct timeval tv = {.tv_sec = 0, .tv_usec = 0}; //wait 0 msec
    if (rd_port == MAX_HUB_PORTS)
    {
        rv = DLL_CALL(canReceive)(h, &m);
        if (rv == 1)
        {
            return 0;
        }
    }
    else
    {
        rv = hub_receive(&p[rd_port], &m);
        if (rv != 1)
        {
            return rv;
        }
    }
    memcpy(&wfds_copy, wfds, sizeof(fd_set));
    rv = select(max_fd + 1, NULL, &wfds_copy, NULL, &tv);
    if (rv <= 0)
    {
        return 0;
    }
    for (i = 0; i < MAX_HUB_PORTS + 1; i++)
    {
        if (i == rd_port)
        {
            fprintf(stderr, "[%d] ", i);
            continue;
        }
        if (p[i].fd < 0 || !FD_ISSET(p[i].fd, &wfds_copy))
        {
            fprintf(stderr, "{%d} ", i);
            continue;
        }
        fprintf(stderr, "<%d> ", i);
        if (i == MAX_HUB_PORTS && h)
        {
            DLL_CALL(canSend)(h, &m);
        }
        else
        {
            hub_send(&p[i], &m);
        }
    }
    print_message(&m);
    return 0;
}

void help(void)
{
    printf("\n\n");
    printf("This is a software hub for the CANFestival library, \n");
    printf("based on the *nix pseudo tty.  It supports up to 16\n");
    printf("connections from clients, and a connection to a CANFestival\n");
    printf("driver, for connection to the outside world.\n");
    printf("\n");
    printf("Basic use is simply to run can_hub.  Without arguments, it\n");
    printf("will use /dev/ptya[0..f] ptys.  You should then run your\n");
    printf("linux CANFestival app using libcanfestival_can_serial.so\n");
    printf("with the bus name /dev/ttyaX, where X is 0..f and unused.\n");
    printf("\n");
    printf("You can alter the pty base with -p /dev/ptyx .\n");
    printf("\n");
    printf("If you want to interface with some other CAN driver, supply\n");
    printf("the option -l /path/to/libcanfestival_can_foo.so .\n");
    printf("The default bus name and baud are /dev/ttyS0 and 125k.\n");
    printf("These can be overridden with -b /dev/{bus name} and -s {baud}.\n");
}

/*UnLoads the dll*/
UNS8 UnLoadCanDriver(LIB_HANDLE handle)
{
    if (handle != NULL)
    {
        dlclose(handle);
        handle = NULL;
        return 0;
    }
    return -1;
}

/*Loads the dll and get funcs ptr*/
LIB_HANDLE LoadCanDriver(char* driver_name)
{
    LIB_HANDLE handle = NULL;
    char* error;
    if (handle == NULL)
    {
        handle = dlopen(driver_name, RTLD_LAZY);
    }
    if (!handle)
    {
        fprintf(stderr, "%s\n", dlerror());
        return NULL;
    }
    /*Get function ptr*/
    DLSYM(canReceive)
    DLSYM(canSend)
    DLSYM(canOpen)
    DLSYM(canChangeBaudRate)
    DLSYM(canClose)
    DLSYM(canfd)
    return handle;
}

/**
*/
int main(int argc, char** argv)
{
    int i, rv, max_fd = 0, ret = 0;
    fd_set rfds, rfds_copy;
    CAN_HANDLE can_h = NULL;
    LIB_HANDLE lib_h = NULL;
    int c;
    extern char* optarg;
    char* can_drv = NULL;
    char* pty_base = "/dev/ptya";
    while ((c = getopt(argc, argv, "-b:s:l:p:h")) != EOF)
    {
        switch (c)
        {
        case 'b':
            if (optarg[0] == 0)
            {
                help();
                exit(1);
            }
            bus_if.busname = optarg;
            break;
        case 's':
            if (optarg[0] == 0)
            {
                help();
                exit(1);
            }
            bus_if.baudrate = optarg;
            break;
        case 'l':
            if (optarg[0] == 0)
            {
                help();
                exit(1);
            }
            can_drv = optarg;
            break;
        case 'p':
            if (optarg[0] == 0)
            {
                help();
                exit(1);
            }
            pty_base = optarg;
            break;
        case 'h':
            help();
            exit(1);
            break;
        default:
            help();
            exit(1);
        }
    }
    FD_ZERO(&rfds);
    hub_ports[MAX_HUB_PORTS].fd = -1;
    if (can_drv)
    {
        lib_h = LoadCanDriver(can_drv);
        if (lib_h == NULL)
        {
            printf("Unable to load library: %s\n", can_drv);
            exit(1);
        }
        can_h = DLL_CALL(canOpen)(&bus_if);
        if (!can_h)
        {
            fprintf(stderr, "canOpen : failed\n");
            exit(1);
        }
        hub_ports[MAX_HUB_PORTS].fd = DLL_CALL(canfd)(can_h);
        FD_SET(hub_ports[MAX_HUB_PORTS].fd, &rfds);
        if (hub_ports[MAX_HUB_PORTS].fd > max_fd)
        {
            max_fd = hub_ports[MAX_HUB_PORTS].fd;
        }
    }
    for (i = 0; i < MAX_HUB_PORTS; i++)
    {
        hub_ports[i].fd = -1;
        hub_ports[i].name = NULL;
        rv = asprintf(&hub_ports[i].name, "%s%x", pty_base, i);
        if (rv < 0)
        {
            fprintf(stderr, "asprintf: %s\n", strerror(errno));
            ret = 1;
            break;
        }
        rv = hub_open(&hub_ports[i]);
        if (rv < 0)
        {
            ret = 1;
            break;
        }
        FD_SET(rv, &rfds);
        if (rv > max_fd)
        {
            max_fd = rv;
        }
    }
    if (ret)
    {
        return ret;
    }
    while (!ret)
    {
        memcpy(&rfds_copy, &rfds, sizeof(rfds));
        rv = select(max_fd + 1, &rfds_copy, NULL, NULL, NULL);
        if (rv < 0)
        {
            //select error
            fprintf(stderr, "select: %s\n", strerror(errno));
            ret = 1;
            continue;
        }
        //as timeout is NULL, must be a rfds set.
        for (i = 0; i < MAX_HUB_PORTS + 1; i++)
        {
            if (hub_ports[i].fd >= 0 &&
                FD_ISSET(hub_ports[i].fd, &rfds_copy))
            {
                rv = read_write(i, hub_ports, can_h, &rfds, max_fd);
                if (rv < 0 && i < MAX_HUB_PORTS)
                {
                    FD_CLR(hub_ports[i].fd, &rfds);
                    hub_close(&hub_ports[i]);
                }
            }
            if (hub_ports[i].fd < 0 && i < MAX_HUB_PORTS)
            {
                rv = hub_open(&hub_ports[i]);
                if (rv >= 0)
                {
                    FD_SET(rv, &rfds);
                    if (rv > max_fd)
                    {
                        max_fd = rv;
                    }
                }
            }
        }
    }
    for (i = 0; i < MAX_HUB_PORTS; i++)
    {
        hub_close(&hub_ports[i]);
    }
    if (hub_ports[MAX_HUB_PORTS].fd >= 0)
    {
        DLL_CALL(canClose)(&bus_if);
    }
    return ret;
}

